#include <iostream>
#include <string>
#include <list>

#include <fiff.hpp>

const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_NONE =       INIT_PAIR( "None    ", "None" );
const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_MODELS =     INIT_PAIR( "Models  ", "Mdls" );
const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_BITMAPS =    INIT_PAIR( "Bitmaps ", "BMPs" );
const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_ANIMATIONS = INIT_PAIR( "Anims   ", "Anis" );
const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_SOUNDS =     INIT_PAIR( "Sounds  ", "Snds" );
const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_ARCHIVES =   INIT_PAIR( "Archives", "Arcs" );
const fiff::sChunkIDPair AVION_ARCHIVE_TYPE_ID_MIXED =      INIT_PAIR( "Mixed   ", "Mixd" );

void print_usage()
{
   std::cout << "Parameters: [archive filename] [archive file]*" << std::endl;
}

void fiff_message( const char * const Type, const char * const String )
{
   std::cerr << "FIFF " << Type << ": \"" << String << "\"" << std::endl;
}

void fiff_warning( const char * const MessageString )
{
   fiff_message( "warning", MessageString );
}
void fiff_error( const char * const MessageString )
{
   fiff_message( "error", MessageString );
}

std::string get_file_extension( const std::string & NameAndPath )
{
   size_t ExtensionDotIndex = NameAndPath.find_last_of( "." );
   if ( ExtensionDotIndex != std::string::npos )
   {
      // If this is the last character
      const size_t NextCharIndex = ExtensionDotIndex + 1;
      if ( NextCharIndex == NameAndPath.length() )
      {
         // Return "." to indicate it doesn't exactly have a blank extension
         return ".";
      }
      else
      {
         return NameAndPath.substr( NextCharIndex );
      }
   }
   return "";
}

bool get_archive_name_and_type_plus_files( std::string & ArchiveName, fiff::sChunkIDPair & ArchiveType,
                                           std::list<const char *> & Files,
                                           const int ArgumentCount, const char * const ArgumentValues[] )
{
   ArchiveName.empty();
   ArchiveType = AVION_ARCHIVE_TYPE_ID_NONE;
   Files.clear();

   // If the first parameter has no extension, then use it
   int FirstFileParameterIndex = 1;
   bool ExplicitlySpecifiedArchiveName = false;
   if ( get_file_extension( ArgumentValues[1] ) == "" )
   {
      ArchiveName.append( ArgumentValues[1] );
      ArchiveName.append( ".avnArchive" );
      FirstFileParameterIndex = 2;
      ExplicitlySpecifiedArchiveName = true;
   }

   // Discern the archive type based on the file extensions
   std::string ArchiveFileTypeExtension = "None";
   bool ExtensionError = false;
   for ( int ArgIndex = FirstFileParameterIndex; ArgIndex < ArgumentCount; ++ArgIndex )
   {
      std::string FileNameAndPath = ArgumentValues[ArgIndex];
      std::string Extension = get_file_extension( FileNameAndPath );

      if ( Extension != "avionMdl" && Extension != "avionTex" && Extension != "avionNrm" &&
           Extension != "avionAni" && Extension != "avnArchive" && Extension != "avionSnd" )
      {
         std::cerr << "Skipping file \"" << FileNameAndPath << ":\" invalid extension." << std::endl;
         ExtensionError = true;
         continue;
      }
      // The file is valid; add it to the list to add
      else
      {
         Files.push_back( ArgumentValues[ArgIndex] );
      }

      if ( ArchiveType == AVION_ARCHIVE_TYPE_ID_NONE )
      {
         if ( Extension == "avionMdl" )
         {
            ArchiveType = AVION_ARCHIVE_TYPE_ID_MODELS;
            ArchiveName = ArchiveName.empty() ? "Models.avnArchive" : ArchiveName;
         }
         else if ( Extension == "avionTex" || Extension == "avoinNrm" )
         {
            ArchiveType = AVION_ARCHIVE_TYPE_ID_BITMAPS;
            if ( Extension == "avionTex" )
            {
               ArchiveName = ArchiveName.empty() ? "Textures.avnArchive" : ArchiveName;
            }
            else
            {
               ArchiveName = ArchiveName.empty() ? "Normals.avnArchive" : ArchiveName;
            }
         }
         else if ( Extension == "avionAni" )
         {
            ArchiveType = AVION_ARCHIVE_TYPE_ID_ANIMATIONS;
            ArchiveName = ArchiveName.empty() ? "Animations.avnArchive" : ArchiveName;
         }
         else if ( Extension == "avionSnd" )
         {
            ArchiveType = AVION_ARCHIVE_TYPE_ID_SOUNDS;
            ArchiveName = ArchiveName.empty() ? "Sounds.avnArchive" : ArchiveName;
         }
         else if ( Extension == "avnArchive" )
         {
            ArchiveType = AVION_ARCHIVE_TYPE_ID_ARCHIVES;
            ArchiveName = ArchiveName.empty() ? "Archives.avnArchive" : ArchiveName;
         }
         ArchiveFileTypeExtension = Extension;
      }
      else if ( Extension != ArchiveFileTypeExtension )
      {
         ArchiveType = AVION_ARCHIVE_TYPE_ID_MIXED;

         // If the file doesn't have an explicitly specified name, change it to "Mixed."
         if ( ExplicitlySpecifiedArchiveName == false )
         {
            ArchiveName = "Mixed.avnArchive";
         }
      }
   }

   if ( ExtensionError )
   {
      std::cerr << " Valid extensions are avionMdl, avionTex, avionNrm, avionAni, avionSnd, and avnArchive." << std::endl;
   }

   // Could we find no valid archive name? If not, abort: there are no valid files to add and no
   // way of knowing what even an empty archive should be called.
   if ( ArchiveName.empty() )
   {
      std::cerr << "No archive name parameter set and no valid files could be added. Aborting!" << std::endl;
      return false;
   }

   return true;
}

int main( const int argc, const char * const argv[] )
{
   fiff::set_allocator( fiff::allocator_new );
   fiff::set_warning_callback( fiff_warning );
   fiff::set_error_callback( fiff_error );

   // Check for valid parameters
   if ( argc < 2 )
   {
      // Remember, the first parameter is the name of the program - the "real" parameters follow
      std::cerr << "No parameters, can't save file!" << std::endl;
      print_usage();
      return 1;
   }

   // Find the name to use for the archive, its type, and the list of files to include in it:
   std::string ArchiveName( "" );
   fiff::sChunkIDPair ArchiveType = AVION_ARCHIVE_TYPE_ID_NONE;
   std::list<const char *> Files;
   if ( get_archive_name_and_type_plus_files( ArchiveName, ArchiveType, Files, argc, argv ) == false )
   {
      return 1;
   }

   // Set the bit mode. May allow setting by command line in the future...
   fiff::eBitMode ArchiveBitMode = fiff::k64;

   // Start the archive file
   const bool FileOpened = fiff::file_begin( ArchiveName.c_str(), fiff::kNewOrOverwrite );
   if ( FileOpened == false )
   {
      std::cerr << "Couldn't open file \"" << ArchiveName << ".\"" << std::endl;
      print_usage();
      return 1;
   }
   // Set our bit mode (so that it'll work automatically in the future)
   fiff::set_write_bit_mode( ArchiveBitMode );
   // Automatically add index = true
   fiff::write_group_begin( ArchiveType, true );

   std::list<const char *>::iterator it;
   for ( it = Files.begin(); it != Files.end(); ++it )
   {
      std::string Extension = get_file_extension( *it );

      // Write out the entire file
      char * FileData = NULL;
      uint64_t FileDataSizeBytes = 0;

      // Read the data from the file
      const bool FileOpened = fiff::file_begin( *it, fiff::kExisting );
      if ( FileOpened == false )
      {
         std::cerr << "Failed to open file \"" << *it << ".\"" << std::endl;
         continue;
      }
      const fiff::eBitMode CurFileBitMode = fiff::get_file_bit_mode();
      if ( CurFileBitMode != ArchiveBitMode )
      {
         std::cerr << "Failed to add file \"" << *it << ",\" it is in a different bit mode (" <<
                      ( CurFileBitMode == fiff::k64 ? "64" : "32" ) << ") than the archive file." << std::endl;
         fiff::file_end();
         continue;
      }

      if ( Extension != "avnArchive" )
      {
         std::string NameS, AuthorS, LicenseS;
         uint64_t CrTime, ModTime;
         bool CreationTimeValid, ModTimeValid;
         if ( fiff::read_object_begin( Extension.c_str() ) )
         {
            const char * const Name = fiff::get_chunk_name();
            const char * const Author = fiff::get_chunk_metadata_author();
            const char * const License = fiff::get_chunk_metadata_license();
            CreationTimeValid = fiff::get_chunk_metadata_creation_time( CrTime );
            ModTimeValid = fiff::get_chunk_metadata_modification_time( ModTime );

            FileDataSizeBytes = fiff::read_chunk_remaining_bytes( FileData );

            NameS = Name == NULL ? "" : Name;
            AuthorS = Author == NULL ? "" : Author;
            LicenseS = License == NULL ? "" : License;

            fiff::read_object_end( Extension.c_str() );
         }
         else
         {
            std::cerr << "Failed to read object with ID \"" << Extension << ".\"" << std::endl;
            continue;
         }
         fiff::file_end();

         if ( NameS.length() == 0 )
         {
            std::cerr << "Can't write object from file \"" << *it << ".\" because it doesn't have an object name!" << std::endl;
            continue;
         }

         if ( AuthorS.length() == 0 || LicenseS.length() == 0 || ModTimeValid == false || CreationTimeValid == false )
         {
            std::cerr << "AvnArchiver requires full metadata on all files. File \"" <<
                         *it<< ".\"" << std::endl << " Missing:" << std::endl;
            if ( AuthorS.length() == 0 )
            {
               std::cerr << " Author" << std::endl;
            }
            if ( LicenseS.length() == 0 )
            {
               std::cerr << " License" << std::endl;
            }
            if ( CreationTimeValid == false )
            {
               std::cerr << " Creation time" << std::endl;
            }
            if ( ModTimeValid == false )
            {
               std::cerr << " Modification time" << std::endl;
            }
            continue;
         }

         fiff::write_object_begin( Extension.c_str(), NameS.c_str() );

         fiff::write_chunk_begin( fiff::METADATA );
            fiff::write_chunk_begin( fiff::AUTHOR );
            fiff::write( AuthorS.c_str(), AuthorS.length() + 1 );
            fiff::write_chunk_end( fiff::AUTHOR );

            fiff::write_chunk_begin( fiff::LICENSE );
            fiff::write( LicenseS.c_str(), LicenseS.length() + 1 );
            fiff::write_chunk_end( fiff::LICENSE );

            fiff::write_chunk_begin( fiff::CREATION_TIME );
            fiff::write( CrTime );
            fiff::write_chunk_end( fiff::CREATION_TIME );

            fiff::write_chunk_begin( fiff::MODIFICATION_TIME );
            fiff::write( ModTime );
            fiff::write_chunk_end( fiff::MODIFICATION_TIME );
         fiff::write_chunk_end( fiff::METADATA );

         if ( FileData != NULL )
         {
            fiff::write( FileData, FileDataSizeBytes );
         }
         delete [] FileData;
         FileData = NULL;
         fiff::write_object_end( Extension.c_str() );
      }
      else
      {
         fiff::read_group_begin();

         FileDataSizeBytes = fiff::read_chunk_remaining_bytes( FileData );
         fiff::uChunkID64 TypeID = fiff::get_chunk_type_ID();
         fiff::sChunkIDPair TypeIDPair;
         TypeIDPair.ID32.value = TypeID.value;
         TypeIDPair.ID64.value = TypeID.value;
         std::string Name = fiff::get_chunk_name();

         fiff::read_group_end();
         fiff::file_end();

         fiff::write_group_begin( TypeIDPair, Name.c_str() );
         if ( FileData != NULL )
         {
            fiff::write( FileData, FileDataSizeBytes );
         }
         delete [] FileData;
         FileData = NULL;
         fiff::write_group_end( TypeIDPair );
      }
   }

   fiff::write_group_end( ArchiveType );
   fiff::file_end();
}
